
package at.fhj.ase.crypto.client.net;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.PrivateKey;

import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;

import at.fhj.ase.crypto.client.security.SecurityController;
import at.fhj.ase.crypto.common.net.MessageHandler;
import at.fhj.ase.crypto.common.util.CommandBuilder;
import at.fhj.ase.crypto.common.util.CryptoUtil;
import at.fhj.ase.crypto.common.util.LogUtil;
import at.fhj.ase.crypto.common.util.NetworkUtil;
import at.fhj.ase.crypto.common.util.StringUtil;

/**
 * Class responsible for all actions relating the connection to the server.<br>
 * We could also say that is is the "central access point" to and from the server.
 * <p>
 * This implies a handling of the messages and errors as well as a sending of the messages back to server.<br>
 * It also saved the server session key.
 * <p>
 * Note that this class implements {@link MessageHandler} as it is responsible for dealing all arriving messages from the socket.<br>
 * Note also that this class is implemented as a Singleton pattern using the enum approach as we do not need multiple, central access points to the server.
 * 
 * @author sfe | www.fh-joanneum.at | Practical Aspects of Cryptography
 */
public enum ServerConnector implements MessageHandler {
  INSTANCE;
  /** The sole logging instance */
  private final Logger logger = LogUtil.getLogger(this);
  /** The sole connection to the server (encapsulates the socket connection) */
  private ClientServerConnection serverConnection;
  /** The server's session key which will later on be used for encrypting and decrypting messages */
  private SecretKeySpec serverSessionkey;

  /**
   * Initializes this {@link ServerConnector} by creating a server connection and starting the authentication process with the server.
   * 
   * @param host the host name we want to create a socket connection with
   * @param port the port number we that belongs to the hostname
   * @throws UnknownHostException if there could not be found a host for the provided <code>hostname</code>
   * @throws IOException if the socket connection could not be established while creating {@link ClientServerConnection}
   */
  public void init(final String host, final int port) throws UnknownHostException, IOException {
    NetworkUtil.checkPortNumber(port);

    logger.debug("Starting client, connecting to " + host + ":" + port);
    serverConnection = ClientServerConnection.create(new Socket(host, port), this);
    startAuthentication();
  }

  /**
   * @see MessageHandler#handleMessage(String)
   */
  public void handleMessage(String msg) {
    if (msg.startsWith(CommandBuilder.DATA)) {
      logger.debug("Retrieved normal chat message, decrypting and printing ...");
      printChatMessage(msg);
    } else if (msg.startsWith(CommandBuilder.SESSION_KEY)) {
      logger.debug("Retrieved encrypted session key from server, saving and signing ...");
      saveSessionKey(msg);

      final byte[] signedSessionKey = signSessionKey();
      sendSignedSessionKey(signedSessionKey);
    } else if (msg.startsWith(CommandBuilder.AUTHENTICATED)) {
      logger.debug("Retrieved verification from server, we are now allowed to encrypt/decrypt messages with session key ...");
      verificationReceived();
    } else if (msg.startsWith(CommandBuilder.DENIED)) {
      logger.debug("Retrieved DENIAL from server, quitting...");
      denialReceived();
    } else {
      logger.error("Retrieved unknown messages from the server, quitting...");
      System.exit(1);
    }
  }

  /**
   * Decrypts and afterwards prints the chat message on console/logger.<br>
   * To decrypt the message, {@link CryptoUtil} is used. To parse the message content, we use {@link CommandBuilder}.
   * 
   * @param msg the message that shall be parsed, decrypted and printed on console
   */
  private void printChatMessage(final String msg) {
    final String encryptedClientMsg = CommandBuilder.parseAndReturnValue(msg, CommandBuilder.DATA, 1);
    try {
      final byte[] plainClientMsg = CryptoUtil.decryptWithSecret(serverSessionkey, CryptoUtil.getBytes(encryptedClientMsg));

      // print the decrypted msg on logger and console, could also be e.g. a GUI
      final String CHAT_MSG = new String(plainClientMsg, "UTF-8");
      logger.debug("Incoming and encrypted chat msg: " + CHAT_MSG);
      System.out.println(CHAT_MSG);
    } catch (Exception e) {
      logger.error("Could not decrypt message with session key", e);
      return;
    }
  }

  /**
   * Starts the authentication process with the server.<br>
   * At first, the public key of this client is sent to server.
   */
  private void startAuthentication() {
    sendPublicKey();
  }

  /**
   * Convenience method sending encrypted chat messages to the server.<br>
   * Note that this is only possible if the client has already been authenticated by the server, otherwise the socket connection will be closed.
   * 
   * @param msgPlainText the message as {@link String} representation and in plain text that shall be encrypted and assembled to a protocol message that is
   * later on sent to server
   */
  void sendEncryptedData(final String msgPlainText) {
    StringUtil.checkEmptyOrNull(msgPlainText);
    try {
      final byte[] encryptedMsg = CryptoUtil.encryptWithSecret(serverSessionkey, msgPlainText.getBytes());
      final String msg = new CommandBuilder(CommandBuilder.DATA).addCommand(CryptoUtil.getStringHex(encryptedMsg)).toString();
      sendMessage(msg);

      logger.debug("Printing self-written message on console...");
      System.out.println(msgPlainText);
    } catch (Exception e) {
      logger.error("Could not encrypt with session key after verification");
      return;
    }
  }

  /**
   * Method called if the client has not been authenticated by the server.<br>
   * Currently it just quits the application with error-code <code>1</code>
   */
  private void denialReceived() {
    System.exit(1);
  }

  /**
   * Parse the arriving message, decrypt and save the session key in this instance.<br>
   * Note that a {@link SecretKeySpec} has to be created, otherwise it does not work (e.g. <code>byte[]</code>)
   * 
   * @param msg the session key that is sent from server
   */
  private void saveSessionKey(String msg) {
    final byte[] serverSessionKey = CryptoUtil.getBytes(CommandBuilder.parseAndReturnValue(msg, CommandBuilder.SESSION_KEY, 1));
    try {
      serverSessionkey = new SecretKeySpec(CryptoUtil.decryptWithPrivate(SecurityController.getInstance().getPrivateKey(), serverSessionKey), CryptoUtil.AES);
    } catch (Exception e) {
      logger.error("Could not save server session key", e);
      return;
    }
  }

  /**
   * Signs the session key with the client's private key using {@link SecurityController}.
   * 
   * @return the signed session key as a byte array
   */
  private byte[] signSessionKey() {
    try {
      final PrivateKey clientPrivateKey = CryptoUtil.getPrivateKey(SecurityController.getInstance().getPrivateKey().getEncoded());
      return CryptoUtil.signWithPrivateKey(clientPrivateKey, serverSessionkey.getEncoded());
    } catch (Exception e) {
      logger.error("Troubles signing session key, returning", e);
      return null;
    }
  }

  /**
   * Sends the signed session key to the server.<br>
   * Again the {@link CommandBuilder} is used to have a valid protocol over the network.<br>
   * The {@link CryptoUtil} performs the cryptographic operations.
   * 
   * @param signedSessionKey the signed session key as a byte array representation
   */
  private void sendSignedSessionKey(byte[] signedSessionKey) {
    final String signedSessionKeyString = CryptoUtil.getStringHex(signedSessionKey);
    final String msg = new CommandBuilder(CommandBuilder.SIGNATURE).addCommand(signedSessionKeyString).toString();
    sendMessage(msg);
  }

  /**
   * Sends the public key to server.<br>
   * Again the {@link CommandBuilder} is used to have a valid protocol over the network.<br>
   * The {@link CryptoUtil} performs the cryptographic operations.
   */
  private void sendPublicKey() {
    final String publicKeyHex = CryptoUtil.getStringHex(SecurityController.getInstance().getPublicKey().getEncoded());
    final String msg = new CommandBuilder(CommandBuilder.PUBLIC_KEY).addCommand(publicKeyHex).toString();
    sendMessage(msg);
  }

  /**
   * Convenience method for sending a message to the server calling {@link ClientServerConnection#getSocketWrapper()}
   * 
   * @param msg the message that shall be sent to server as {@link String} representation
   */
  private void sendMessage(final String msg) {
    serverConnection.getSocketWrapper().sendMessage(msg);
  }

  /** As soon as the verification from server has been received, the reading from input can be started by initiation {@link ConsoleReaderThread} */
  private void verificationReceived() {
    new ConsoleReaderThread(this).start();
  }

  /**
   * @see MessageHandler#handleError(String)
   */
  public void handleError(String errorMsg) {
    logger.error("Received socket error: " + errorMsg + ", quitting application...");
    System.exit(1);
  }
}
